1   // Licensed under the Apache License, Version 2.0 (the "License");
2   // you may not use this file except in compliance with the License.
3   // You may obtain a copy of the License at
4   //
5   //     http://www.apache.org/licenses/LICENSE-2.0
6   //
7   // Unless required by applicable law or agreed to in writing, software
8   // distributed under the License is distributed on an "AS IS" BASIS,
9   // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10  // See the License for the specific language governing permissions and
11  // limitations under the License.
12  
13  package org.apache.tapestry5.ioc.internal.util;
14  
15  import org.apache.tapestry5.ioc.Invokable;
16  
17  import java.util.concurrent.TimeUnit;
18  import java.util.concurrent.locks.ReadWriteLock;
19  import java.util.concurrent.locks.ReentrantReadWriteLock;
20  
21  /**
22   * A barrier used to execute code in a context where it is guarded by read/write locks. In addition, handles upgrading
23   * read locks to write locks (and vice versa). Execution of code within a lock is in terms of a {@link Runnable} object
24   * (that returns no value), or a {@link Invokable} object (which does return a value).
25   */
26  public class ConcurrentBarrier
27  {
28      private final ReadWriteLock lock = new ReentrantReadWriteLock();
29  
30      /**
31       * This is, of course, a bit of a problem. We don't have an avenue for ensuring that this ThreadLocal is destroyed
32       * at the end of the request, and that means a thread can hold a reference to the class and the class loader which
33       * loaded it. This may cause redeployment problems (leaked classes and class loaders). Apparently JDK 1.6 provides
34       * the APIs to check to see if the current thread has a read lock. So, we tend to remove the TL, rather than set its
35       * value to false.
36       */
37      private static class ThreadBoolean extends ThreadLocal<Boolean>
38      {
39          @Override
40          protected Boolean initialValue()
41          {
42              return false;
43          }
44      }
45  
46      private final ThreadBoolean threadHasReadLock = new ThreadBoolean();
47  
48      /**
49       * Invokes the object after acquiring the read lock (if necessary). If invoked when the read lock has not yet been
50       * acquired, then the lock is acquired for the duration of the call. If the lock has already been acquired, then the
51       * status of the lock is not changed.
52       *
53       * TODO: Check to see if the write lock is acquired and <em>not</em> acquire the read lock in that situation.
54       * Currently this code is not re-entrant. If a write lock is already acquired and the thread attempts to get the
55       * read lock, then the thread will hang. For the moment, all the uses of ConcurrentBarrier are coded in such a way
56       * that reentrant locks are not a problem.
57       *
58       * @param <T>
59       * @param invokable
60       * @return the result of invoking the invokable
61       */
62      public <T> T withRead(Invokable<T> invokable)
63      {
64          boolean readLockedAtEntry;
65  
66          synchronized (threadHasReadLock)
67          {
68              readLockedAtEntry = threadHasReadLock.get();
69          }
70  
71          if (!readLockedAtEntry)
72          {
73              lock.readLock().lock();
74  
75              synchronized (threadHasReadLock)
76              {
77                  threadHasReadLock.set(true);
78              }
79          }
80  
81          try
82          {
83              return invokable.invoke();
84          }
85          finally
86          {
87              if (!readLockedAtEntry)
88              {
89                  lock.readLock().unlock();
90  
91                  synchronized (threadHasReadLock)
92                  {
93                      threadHasReadLock.remove();
94                  }
95              }
96          }
97      }
98  
99      /**
100      * As with {@link #withRead(Invokable)}, creating an {@link Invokable} wrapper around the runnable object.
101      */
102     public void withRead(final Runnable runnable)
103     {
104         Invokable<Void> invokable = new Invokable<Void>()
105         {
106             @Override
107             public Void invoke()
108             {
109                 runnable.run();
110 
111                 return null;
112             }
113         };
114 
115         withRead(invokable);
116     }
117 
118     /**
119      * Acquires the exclusive write lock before invoking the Invokable. The code will be executed exclusively, no other
120      * reader or writer threads will exist (they will be blocked waiting for the lock). If the current thread has a read
121      * lock, it is released before attempting to acquire the write lock, and re-acquired after the write lock is
122      * released. Note that in that short window, between releasing the read lock and acquiring the write lock, it is
123      * entirely possible that some other thread will sneak in and do some work, so the {@link Invokable} object should
124      * be prepared for cases where the state has changed slightly, despite holding the read lock. This usually manifests
125      * as race conditions where either a) some parallel unrelated bit of work has occured or b) duplicate work has
126      * occured. The latter is only problematic if the operation is very expensive.
127      *
128      * @param <T>
129      * @param invokable
130      */
131     public <T> T withWrite(Invokable<T> invokable)
132     {
133         boolean readLockedAtEntry = releaseReadLock();
134 
135         lock.writeLock().lock();
136 
137         try
138         {
139             return invokable.invoke();
140         }
141         finally
142         {
143             lock.writeLock().unlock();
144             restoreReadLock(readLockedAtEntry);
145         }
146     }
147 
148     private boolean releaseReadLock()
149     {
150         boolean readLockedAtEntry;
151 
152         synchronized (threadHasReadLock)
153         {
154             readLockedAtEntry = threadHasReadLock.get();
155         }
156 
157         if (readLockedAtEntry)
158         {
159             lock.readLock().unlock();
160 
161             synchronized (threadHasReadLock)
162             {
163                 threadHasReadLock.set(false);
164             }
165         }
166 
167         return readLockedAtEntry;
168     }
169 
170     private void restoreReadLock(boolean readLockedAtEntry)
171     {
172         if (readLockedAtEntry)
173         {
174             lock.readLock().lock();
175 
176             synchronized (threadHasReadLock)
177             {
178                 threadHasReadLock.set(true);
179             }
180         }
181         else
182         {
183             synchronized (threadHasReadLock)
184             {
185                 threadHasReadLock.remove();
186             }
187         }
188     }
189 
190     /**
191      * As with {@link #withWrite(Invokable)}, creating an {@link Invokable} wrapper around the runnable object.
192      */
193     public void withWrite(final Runnable runnable)
194     {
195         Invokable<Void> invokable = new Invokable<Void>()
196         {
197             @Override
198             public Void invoke()
199             {
200                 runnable.run();
201 
202                 return null;
203             }
204         };
205 
206         withWrite(invokable);
207     }
208 
209     /**
210      * Try to aquire the exclusive write lock and invoke the Runnable. If the write lock is obtained within the specfied
211      * timeout, then this method behaves as {@link #withWrite(Runnable)} and will return true. If the write lock is not
212      * obtained within the timeout then the runnable is never invoked and the method will return false.
213      *
214      * @param runnable    Runnable object to execute inside the write lock.
215      * @param timeout     Time to wait for write lock.
216      * @param timeoutUnit Units of timeout.
217      * @return true if lock was obtained and the runnable executed, or false otherwise.
218      */
219     public boolean tryWithWrite(final Runnable runnable, long timeout, TimeUnit timeoutUnit)
220     {
221         boolean readLockedAtEntry = releaseReadLock();
222 
223         boolean obtainedLock = false;
224 
225         try
226         {
227             try
228             {
229                 obtainedLock = lock.writeLock().tryLock(timeout, timeoutUnit);
230 
231                 if (obtainedLock) runnable.run();
232 
233             }
234             catch (InterruptedException e)
235             {
236                 obtainedLock = false;
237             }
238             finally
239             {
240                 if (obtainedLock) lock.writeLock().unlock();
241             }
242         }
243         finally
244         {
245             restoreReadLock(readLockedAtEntry);
246         }
247 
248         return obtainedLock;
249     }
250 
251 }